// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: fengyang

#pragma once

#include <string>

#include <rapidjson/document.h>
#include <rapidjson/rapidjson.h>

#include "coin/proto/coin_market_enums.pb.h"
#include "coin2/base/conversion.h"
#include "coin2/base/time.h"
#include "coin2/exchange/base/feed/update.h"
#include "coin2/exchange/base/feed/processor.h"
#include "coin2/exchange/base/feed_util/book_builder.h"
#include "coin2/exchange/base/feed_util/trade.h"

namespace coin2::exchange::bybit_futures::feed_v3 {
namespace impl {
using coin2::exchange::base::market::ExchangeType;
using coin2::exchange::base::market::MarketType;
using coin2::exchange::base::feed::FeedParsingProcessor;
using coin2::exchange::base::feed_util::Index;

class BybitFeedUpdateParser {
 public:
  static bool
  ParseBook(
      BookBuilder* book,
      int64_t exchange_timestamp,
      const rapidjson::Document& document,
      bool light_bbo_move_xbp,
      std::string_view native_symbol,
      int64_t packet_timestamp) {
    if (exchange_timestamp < book->Timestamp()) {
      SPDLOG_WARN_EVERY_NS(
          5'000'000'000L,
          exchange_timestamp,
          "{} {} {} {}, {}, {}",
          "Book of",
          native_symbol,
          "ET timestamp inverse!",
          exchange_timestamp,
          book->Timestamp(),
          packet_timestamp);
      return false;
    }
    const std::string& type = document["type"].GetString();
    if (type == "snapshot") {
      return BybitFeedUpdateParser::ParseSnapshot(book, exchange_timestamp, document["data"]);
    } else if (type == "delta") {
      return BybitFeedUpdateParser::ParseDelta(book, exchange_timestamp, document["data"], light_bbo_move_xbp);
    } else {
      return false;
    }
  }

  static bool
  ParseSnapshot(BookBuilder* book, int64_t ts, const rapidjson::Value& book_json) {
    auto tx = book->NewTransaction(ts);
    tx.Clear();
    UpdateBook(book_json, &tx);
    return true;
  }

  static bool
  ParseDelta(BookBuilder* book, int64_t ts, const rapidjson::Value& book_json,
      bool light_bbo_move_xbp) {
    auto tx = book->NewTransaction(ts);
    if (light_bbo_move_xbp) {
      tx.BboClear();
    }
    UpdateBook(book_json, &tx);

    return true;
  }

  static void ParseTrade(const rapidjson::Value& trade_json, Trade* trade) {
    trade->price = stod(trade_json["p"].GetString());
    trade->fill_qty = stod(trade_json["v"].GetString());
    trade->side = ParseTradeSide(trade_json["S"].GetString());
    int64_t ts = stoll(trade_json["T"].GetString()) * 1'000'000LL;
    trade->timestamp = ts;
  }

  static void ParseOpenInterest(
      const rapidjson::Value& ticker_json,
      OpenInterest* open_interest,
      int64_t ts) {
    open_interest->timestamp = ts;
    open_interest->open_interest_qty = stod(ticker_json["openInterest"].GetString());
  }

  static void ParseMarkPrice(
      const rapidjson::Value& ticker_json,
      MarkPrice* mark_price,
      int64_t ts) {
    mark_price->timestamp = ts;
    mark_price->mark_price = stod(ticker_json["markPrice"].GetString());
  }

  static void ParseIndex(
      const rapidjson::Value& ticker_json,
      Index* index,
      int64_t ts) {
    index->timestamp = ts;
    index->price = stod(ticker_json["indexPrice"].GetString());
  }

  static bool ParseFundingRate(
      const rapidjson::Value& ticker_json,
      FundingRate* funding_rate,
      int64_t ts,
      bool is_snapshot) {
    bool updated = false;
    funding_rate->timestamp = ts;
    if (ticker_json.HasMember("fundingRate") &&
        ticker_json["fundingRate"].GetStringLength() > 0) {
      funding_rate->funding_rate = stod(ticker_json["fundingRate"].GetString());
      updated = true;
    }
    if (ticker_json.HasMember("nextFundingTime") &&
        ticker_json["nextFundingTime"].GetStringLength() > 0) {
      const std::string nextFundingTime_str = ticker_json["nextFundingTime"].GetString();
      if (nextFundingTime_str.find("T") != std::string::npos) {
        funding_rate->funding_time =TimestampFromIso8601(nextFundingTime_str);
      } else {
        funding_rate->funding_time = stoll(nextFundingTime_str)*1'000'000LL;
      }
      updated = true;
    }
    if (ticker_json.HasMember("predictedFundingRate") &&
        ticker_json["predictedFundingRate"].GetStringLength() > 0) {
      funding_rate->estimated_rate = stod(ticker_json["predictedFundingRate"].GetString());
      updated = true;
    }

    return updated;
  }

  static bool ParseKlineUrl(
      std::string_view url,
      std::string* native_symbol,
      fastfeed::proto::KlineInterval* interval,
      int* page) {
    if (url.find("/kline?") == std::string::npos) {
      return false;
    }

    auto parameters = FeedParsingProcessor::GetRequestParameters(url);

    *native_symbol = parameters["symbol"];
    *interval = ParseKlineInterval(parameters["interval"]);

    *page = -1;
    auto iter = parameters.find("page");
    if (iter != parameters.end()) {
      *page = std::stoi(iter->second);
    }

    return true;
  }

  static fastfeed::proto::KlineInterval ParseKlineInterval(const std::string& period) {
    static std::map<std::string, fastfeed::proto::KlineInterval> interval_map = {
      {"1", fastfeed::proto::KLINE_INTERVAL_1MIN},
      {"3", fastfeed::proto::KLINE_INTERVAL_3MIN},
      {"5", fastfeed::proto::KLINE_INTERVAL_5MIN},
      {"15", fastfeed::proto::KLINE_INTERVAL_15MIN},
      {"30", fastfeed::proto::KLINE_INTERVAL_30MIN},
      {"60", fastfeed::proto::KLINE_INTERVAL_1HOUR},
      {"120", fastfeed::proto::KLINE_INTERVAL_2HOUR},
      {"240", fastfeed::proto::KLINE_INTERVAL_4HOUR},
      {"360", fastfeed::proto::KLINE_INTERVAL_6HOUR},
      {"720", fastfeed::proto::KLINE_INTERVAL_12HOUR},
      {"D", fastfeed::proto::KLINE_INTERVAL_1DAY},
      {"W", fastfeed::proto::KLINE_INTERVAL_1WEEK},
      {"M", fastfeed::proto::KLINE_INTERVAL_1MON}
    };
    auto iter = interval_map.find(period);
    if (iter == interval_map.end()) {
      NOTREACHED() << "Not support interval " << period;
      return fastfeed::proto::KLINE_INTERVAL_INVALID;
    }

    return iter->second;
  }

  static bool Parse1Kline(
      const rapidjson::Value& kline_data,
      Klines* klines,
      int64_t timestamp,
      fastfeed::proto::KlineInterval interval) {
    CHECK_EQ(kline_data.Size(), 7);
    int64_t open_timestamp = stoll(kline_data[0].GetString()) * 1'000'000L;
    auto result = klines->AddKline(open_timestamp);
    if (!result) return false;

    Kline* kline;
    bool freezed;
    std::tie(kline, freezed) = result.value();

    kline->open = stod(kline_data[1].GetString());
    kline->high = stod(kline_data[2].GetString());
    kline->low = stod(kline_data[3].GetString());
    kline->close = stod(kline_data[4].GetString());
    kline->volume = stod(kline_data[5].GetString());
    kline->turnover = stod(kline_data[6].GetString());
    kline->timestamp = timestamp;
    kline->interval = interval;
    return freezed;
  }

 private:
  static coin2::exchange::feed::TradeSide ParseTradeSide(const std::string& side) {
    if (side == "Buy") {
      return coin2::exchange::feed::TRADE_BUY_SIDE;
    } else if (side == "Sell") {
      return coin2::exchange::feed::TRADE_SELL_SIDE;
    } else {
      throw std::runtime_error("[Bybit Feed Processor] unknown side: " + side);
    }
  }

  static void
  UpdateBook(const rapidjson::Value& book_json, Transaction* tx) {
    for (const auto& ask_json : book_json["a"].GetArray()) {
      tx->UpdateAsk(stod(ask_json[0].GetString()), stod(ask_json[1].GetString()));
    }

    for (const auto& bid_json : book_json["b"].GetArray()) {
      tx->UpdateBid(stod(bid_json[0].GetString()), stod(bid_json[1].GetString()));
    }
  }
};
}  // namespace impl

using impl::BybitFeedUpdateParser;
}  // namespace coin2::exchange::bybit_futures::feed_v3
